/**
 * This plugin simplifies and standardizes error reporting and form validation.
 * There can be two types of errors (user input and the second one is system).
 * User input errors can be displayed next to the particular field or as a global one.
 * System errors are displayed in the global place.
 * Plugin can also optionally display success message. For example when form submission is successful.
 * <p></p>
 * The whole reporting works within a particular selector.
 * <p></p>
 * This plugin works with named fields. These are the ones which is possible to uniquely
 * identify just by the name.
 * Note: If there is need to implement something more powerful than named field, framework can be extended.
 * <p></p>
 * The response JSON which is processed as error if it is object and contains the following properties:
 * {
 *   errors : {
 *     global : "here_the_error_message",
 *     namedFields : {
 *       field1 : "message_for_field_1",
 *       field2 : "message_for_field_2",
 *       ...
 *     }
 *   }
 * }
 * Where "global" and "namedFields" objects are optional. Errors will be binded according this.
 * 
 * @author radek.hecl
 * @version 2.1.0
 */
var ErrorReporter = this.ErrorReporter = function(selector, options) {
  options = $.extend({}, this.defaults, options);
  this.privateStorage = {};
  
  var addSuccessFn = function() {
    var id = new Date().getTime();
    var alert = '<div class="alert alert-success" data-id="' + id + '">' + options.successMessage + '</div>';
    if ($(selector).is('form')) {
      $(selector).prepend(alert);
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
      $(selector).before(alert);
    }
    else {
      window.alert('Success is not applicable for selector: ' + selector);
    }

    if (options.successTimeout !== 0) {
      window.setTimeout(function () { removeSuccess(id); }, options.successTimeout);
    }  
  };
  
  //register private call backs
  this.privateStorage.addSuccessCallback = function() {
    addSuccessFn();
  };
  
  this.privateStorage.clearErrorsCallback = function() {
    removeGlobalErrors();
    if ($(selector).is('form')) {
      // bootstrap 2
      $(selector).find('.error .help-inline').remove();
      $(selector).find('.error.help-inline').remove();
      $(selector).find('.error').removeClass('error');

      // bootstrap 3
      $(selector).find('.has-error .help-block').remove();
      $(selector).find('.has-error.help-block').remove();
      $(selector).find('.has-error').removeClass('has-error');
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
    }
    else {
      window.alert('ClearErrors is not applicable for selector: ' + selector);
    }
  };
  
  this.privateStorage.validateNamedFieldCallback = function(fieldName, regexp, error) {
    var text = $(getFieldSelector(fieldName)).val();
    if (!regexp.exec(text)) {
      addNamedFieldError(fieldName, error);
    }
  };
  
  this.privateStorage.validateNamedFieldsMatchCallback = function(field1Name, field2Name, error1, error2) {
    var text1 = $(getFieldSelector(field1Name)).val();
    var text2 = $(getFieldSelector(field2Name)).val();
    if (text1 !== text2) {
      addNamedFieldError(field1Name, error1);
      addNamedFieldError(field2Name, error2);
    }
  };
  
  this.privateStorage.validateNamedCheckboxCheckedCallback = function(checkboxName, error) {
    if (!$(getFieldSelector(checkboxName)).prop("checked")) {
      addNamedFieldError(checkboxName, error);
    }
  };

  this.privateStorage.isErrorCallback = function() {
    if ($(selector).is('form')) {
      // bootstrap 2
      if ($(selector + ' .error').length > 0) { return true; }
      if ($(selector + ' .alert-error').length > 0) { return true; }
      // bootstrap 3
      if ($(selector + ' .has-error').length > 0) { return true; }
      return false;
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
      if ($(selector).prevAll('div.alert-error').length > 0) { return true; }
      return false;
    }
    else {
      window.alert('isError is not implemented for selector: ' + selector);
    }
  };
  
  this.privateStorage.isErrorOnNamedFieldCallback = function(fieldName) {
    if ($(selector).hasClass('form-horizontal')) {
      var res = false;
      // bootstrap 2
      res = res || $(getFieldSelector(fieldName)).parent().parent().hasClass('error');
      // bootstrap 3
      res = res || $(getFieldSelector(fieldName)).parent().parent().hasClass('has-error');
      return res;
    }
    else if ($(selector).hasClass('form-inline')) {
      var next = $(getFieldSelector(fieldName)).next();
      if (next.is('span') && next.hasClass('help-inline') && next.hasClass('error')) {
        return true;
      }
      return false;
    }
    else {
      window.alert('Named fields are not applicable for selector: ' + selector);
    }
  };

  this.privateStorage.getJSONCallback = function(url, data, success) {
    if (success == null) { success = function(data) {}; }
    var errorFn = function () {
      addGlobalError(options.errorMessage);
    };
    var successFn = function(data) {
      if (data.errors != null) {
        if (data.errors.global != null) {
          addGlobalError(data.errors.global);
        }
        if (data.errors.namedFields != null) {
          $.each(data.errors.namedFields, function(name, msg) {
            addNamedFieldError(name, msg);
          });
        }
      }
      else {
        if (options.reportAjaxSuccess == true) {
        	addSuccessFn();
        }
        success(data);
      }
    };
    return $.getJSON(url, data, successFn).error(errorFn);
  };

  this.privateStorage.postJSONCallback = function(url, data, success) {
    if (success == null) { success = function(data) {}; }
    var errorFn = function () {
      addGlobalError(options.errorMessage);
    };
    var successFn = function(data) {
      if (data.errors != null) {
        if (data.errors.global != null) {
          addGlobalError(data.errors.global);
        }
        if (data.errors.namedFields != null) {
          $.each(data.errors.namedFields, function(name, msg) {
            addNamedFieldError(name, msg);
          });
        }
      }
      else {
        if (options.reportAjaxSuccess == true) {
          addSuccessFn();
        }
        success(data);
      }
    };
    if ((!!window.FormData) && (data instanceof FormData)){
      return $.ajax({
        url         : url,
        type        : 'POST',
        dataType    : 'json',        
        xhr: function() {
          var xhr = new window.XMLHttpRequest();
          return xhr;
        },
        success     : successFn,
        error       : errorFn,
        data        : data,
        cache       : false,
        contentType : false,
        processData : false
      });
    }
    else {
      return $.post(url, data, successFn, 'json').error(errorFn);
    }
  };

  /**
   * Removes the specified success alert.
   * @param {String} id id of the success alert to remove 
   */
  function removeSuccess(id) {
    if ($(selector).is('form')) {
      var slc = selector + ' div.alert-success[data-id="' + id + '"]';
      $(slc).hide('slow', function() { $(slc).remove(); });
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
      var slc = $(selector).prevAll('div.alert-success[data-id="' + id + '"]');
      $(slc).hide('slow', function() { $(slc).remove(); });
    }
    else {
      window.alert('Success is not applicable for selector: ' + selector);
    }
  }
  
  /**
   * Returns the selector for the specified field by name.
   * @param {String} name name of the field
   * @returns {String} selector for the specified field
   */
  function getFieldSelector(name) {
    return selector + ' input[name="' + name + '"],' + selector + ' select[name="' + name + '"],' +
        selector + ' textarea[name="' + name + '"]';
  }
  
  /**
   * Adds global error.
   * @param {String} error error message
   */
  function addGlobalError(error) {
	if (options.globalErrorPolicy == 'single') {
      removeGlobalErrors();
	}
    var alert = '<div class="alert alert-error alert-danger">' + error + '</div>';
    if ($(selector).is('form')) {
      $(selector).prepend(alert);
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
      $(selector).before(alert);
    }
    else {
      window.alert('Global error is not applicable for selector: ' + selector);
    }
  };
  
  /**
   * Adds error to the named field. If there is some previous error, then it will be replaced.
   * @param {String} fieldName name of the field
   * @param {String} error error message
   */
  function addNamedFieldError(fieldName, error) {
    if ($(selector).hasClass('form-horizontal')) {
      var par1 = $(getFieldSelector(fieldName)).parent();
      var par2 = par1.parent();
      // adjust to cover check boxes
      if (par1.is('label') && par1.hasClass('checkbox')) {
    	// bootstrap 2 checkbox case
        par2 = par2.parent();
        par1 = par1.parent();
      }
      if (par1.is('label') && par2.hasClass('checkbox')) {
    	// bootstrap 3 checkbox case
        par2 = par2.parent().parent();
      }
      
      // add errors
      if (par1.hasClass('controls') && par2.hasClass('control-group')) {
    	// bootstrap 2
        var help = '<span class="help-inline">' + error + '</span>';
        par2.addClass('error');
        par1.children('.help-inline').remove();
        par1.append(help);
      }
      else if (par2.hasClass('form-group')) {
    	  // bootstrap 3
          var help = '<span class="help-block">' + error + '</span>';
          par2.addClass('has-error');
          par1.children('.help-block').remove();
          par1.append(help);
      }
    }
    else if ($(selector).hasClass('form-inline')) {
      var input = $(getFieldSelector(fieldName));
      var help = '<span class="help-inline error">' + error + '</span>';
      var next = input.next();
      if (next.is('span') && next.hasClass('help-inline') && next.hasClass('error')) {
        next.remove();
      }
      input.after(help);
    }
    else {
      window.alert('Named fields are not applicable for selector: ' + selector);
    }
  };

  /**
   * Removes all the global errors.
   */
  function removeGlobalErrors() {
    if ($(selector).is('form')) {
      $(selector + ' div.alert-error').remove();
    }
    else if ($(selector).is('table') || $(selector).is('div')) {
      $(selector).prevAll('div.alert-error').remove();
    }
    else {
      window.alert('ClearGlobalErrors is not applicable for selector: ' + selector);
    }
  };

};

ErrorReporter.prototype = {

  defaults : {

	/**
	 * Default error message which is used when no specified message is provided.
	 */
    errorMessage      : 'Error',
    
    /**
     * Message which is used when operation is successful.
     */
    successMessage    : 'Success',
    
    /**
     * Policy for displaying the global errors.
     * Allowed values are:
     * <ul>
     *   <li>multiple - multiple errors can be displayed</li>
     *   <li>single - only last global error is displayed, whenever new error is requested, the old one is removed</li>
     * </ul>
     */
    globalErrorPolicy : 'multiple',
    
    /**
     * Defines whether automatically report the success of the ajax calls through this object or not.
     */
    reportAjaxSuccess : false,
    
    /**
     * Timeout in milliseconds for the success alert to automatically disappear, 0 means never.
     */
    successTimeout    : 5000
  },

  /**
   * Clears all the global and field errors in the realm of this object.
   */
  clearErrors : function() {
    this.privateStorage.clearErrorsCallback();
  },

  /**
   * Manually adds success message to the realm of this object.
   * Flag reportAjaxSuccess does not have any effect if this method is called.
   * Success automatically disappear after successTimeout.
   * This method doesn't delete any error or previous success.
   */
  addSuccess : function() {
    this.privateStorage.addSuccessCallback();
  },
  
  /**
   * Validates the named field and if there adds the error if validation fail.
   * @param {String} fieldName the name of the field to validate
   * @param {Regexp} regexp regexp which is used to the validation
   * @param {String} error message which is used in case of error 
   */
  validateNamedField : function(fieldName, regexp, error) {
    this.privateStorage.validateNamedFieldCallback(fieldName, regexp, error);
  },
  
  /**
   * Validates if the two named fields match.
   * @param {String} field1Name the name of the first field
   * @param {String} field2Name the name of the second field
   * @param {String} error1 error message for the first field if test fail
   * @param {String} error2 error message for the seconf field if test fail
   */
  validateNamedFieldsMatch : function(field1Name, field2Name, error1, error2) {
    this.privateStorage.validateNamedFieldsMatchCallback(field1Name, field2Name, error1, error2);
  },
  
  /**
   * Validates named checkbox to be checked.
   * @param {String} checkboxName the name of the checboc
   * @param {String} error error message in case checkbox is not checked
   */
  validateNamedCheckboxChecked : function(checkboxName, error) {
    this.privateStorage.validateNamedCheckboxCheckedCallback(checkboxName, error);
  },
  
  /**
   * Returns whether there is any error or not. It can be on the field or global error.
   * @returns {Boolean} whether there is any error or not
   */
  isError : function() {
    return this.privateStorage.isErrorCallback();
  },
  
  /**
   * Returns whether named field has error or not.
   * @param {String} fieldName name of the field 
   * @returns {Boolean} whether the named field has error or not
   */
  isErrorOnNamedField : function(fieldName) {
    return this.privateStorage.isErrorOnNamedFieldCallback(fieldName);
  },
  
  /**
   * Wrapper around jQuery.getJSON error method. This function automatically handles
   * the displaying of the errors and success (if set). Client needs only to deal with data.
   * 
   * @param {String} url url which is passed to $.getJSON method
   * @param {Object} data data which are passed to the $.getJSON method
   * @param {function(Object)} success function which is passed to the $.getJSON method
   * @return {Object} object which is returned by $.getJSON method
   * @see jQuery.getJSON
   */
  getJSON : function(url, data, success) {
    return this.privateStorage.getJSONCallback(url, data, success);
  },

  /**
   * Wrapper around jQuery.postJSON error method. This function automatically handles
   * the displaying of the errors and success (if set). Client needs only to deal with data.
   * 
   * @param {String} url url which is passed to $.postJSON method
   * @param {Object|FormData} data data which are passed to the $.postJSON method,
   *     can be FormData with attached files if browser supports this type of object (IE7,8,9 doesn't support it) 
   * @param {function(Object)} success function which is passed to the $.getJSON method
   * @return {Object} object which is returned by $.getJSON method
   * @see jQuery.postJSON
   */
  postJSON : function(url, data, success) {
    return this.privateStorage.postJSONCallback(url, data, success);
  },

  // !!!
  // Private storage - DO NOT ACCESS THIS OBJECT FROM OUTSIDE
  // This storage is used to simulate the private objects.
  // Therefore only the internal methods should access it.
  privateStorage : {}
};

jQuery.addObject('errorReporter', ErrorReporter);